using System;
using System.Diagnostics;
using SunflowSharp.Core;
using SunflowSharp.Image;
using SunflowSharp.Maths;

namespace SunflowSharp.Core.Primitive
{

    public class CornellBox : PrimitiveList, IShader, LightSource
    {
        private float minX, minY, minZ;
        private float maxX, maxY, maxZ;
        private Color left, right, top, bottom, back;
        private Color radiance;
        private int samples;
        private float lxmin, lymin, lxmax, lymax;
        private float area;
        private BoundingBox lightBounds;

        public CornellBox()
        {
            updateGeometry(new Point3(-1, -1, -1), new Point3(1, 1, 1));

            // cube colors
            left = new Color(0.80f, 0.25f, 0.25f);
            right = new Color(0.25f, 0.25f, 0.80f);
            Color gray = new Color(0.70f, 0.70f, 0.70f);
            top = bottom = back = gray;

            // light source
            radiance = Color.WHITE;
            samples = 16;
        }

        private void updateGeometry(Point3 c0, Point3 c1)
        {
            // figure out cube extents
            lightBounds = new BoundingBox(c0);
            lightBounds.include(c1);

            // cube extents
            minX = lightBounds.getMinimum().x;
            minY = lightBounds.getMinimum().y;
            minZ = lightBounds.getMinimum().z;
            maxX = lightBounds.getMaximum().x;
            maxY = lightBounds.getMaximum().y;
            maxZ = lightBounds.getMaximum().z;

            // work around epsilon problems for light test
            lightBounds.enlargeUlps();

            // light source geometry
            lxmin = maxX / 3 + 2 * minX / 3;
            lxmax = minX / 3 + 2 * maxX / 3;
            lymin = maxY / 3 + 2 * minY / 3;
            lymax = minY / 3 + 2 * maxY / 3;
            area = (lxmax - lxmin) * (lymax - lymin);
        }

        public bool update(ParameterList pl, SunflowAPI api)
        {
            Point3 corner0 = pl.getPoint("corner0", null);
            Point3 corner1 = pl.getPoint("corner1", null);
            if (corner0 != null && corner1 != null)
            {
                updateGeometry(corner0, corner1);
            }

            // shader colors
            left = pl.getColor("leftColor", left);
            right = pl.getColor("rightColor", right);
            top = pl.getColor("topColor", top);
            bottom = pl.getColor("bottomColor", bottom);
            back = pl.getColor("backColor", back);

            // light
            radiance = pl.getColor("radiance", radiance);
            samples = pl.getInt("samples", samples);
            return true;
        }

        public void init(string name, SunflowAPI api)
        {
            // register with the api properly
            api.geometry(name, this);
            api.shader(name + ".shader", this);
            api.parameter("shaders", name + ".shader");
            api.instance(name + ".instance", name);
            api.light(name + ".light", this);
        }

        public BoundingBox getBounds()
        {
            return lightBounds;
        }

        public float getBound(int i)
        {
            switch (i)
            {
                case 0:
                    return minX;
                case 1:
                    return maxX;
                case 2:
                    return minY;
                case 3:
                    return maxY;
                case 4:
                    return minZ;
                case 5:
                    return maxZ;
                default:
                    return 0;
            }
        }

        public bool intersects(BoundingBox box)
        {
            // this could be optimized
            BoundingBox b = new BoundingBox();
            b.include(new Point3(minX, minY, minZ));
            b.include(new Point3(maxX, maxY, maxZ));
            if (b.intersects(box))
            {
                // the box is overlapping or enclosed
                if (!b.contains(new Point3(box.getMinimum().x, box.getMinimum().y, box.getMinimum().z)))
                    return true;
                if (!b.contains(new Point3(box.getMinimum().x, box.getMinimum().y, box.getMaximum().z)))
                    return true;
                if (!b.contains(new Point3(box.getMinimum().x, box.getMaximum().y, box.getMinimum().z)))
                    return true;
                if (!b.contains(new Point3(box.getMinimum().x, box.getMaximum().y, box.getMaximum().z)))
                    return true;
                if (!b.contains(new Point3(box.getMaximum().x, box.getMinimum().y, box.getMinimum().z)))
                    return true;
                if (!b.contains(new Point3(box.getMaximum().x, box.getMinimum().y, box.getMaximum().z)))
                    return true;
                if (!b.contains(new Point3(box.getMaximum().x, box.getMaximum().y, box.getMinimum().z)))
                    return true;
                if (!b.contains(new Point3(box.getMaximum().x, box.getMaximum().y, box.getMaximum().z)))
                    return true;
                // all vertices of the box are inside - the surface of the box is
                // not intersected
            }
            return false;
        }

        public void prepareShadingState(ShadingState state)
        {
            state.init();
            state.getRay().getPoint(state.getPoint());
            int n = state.getPrimitiveID();
            switch (n)
            {
                case 0:
                    state.getNormal().set(new Vector3(1, 0, 0));
                    break;
                case 1:
                    state.getNormal().set(new Vector3(-1, 0, 0));
                    break;
                case 2:
                    state.getNormal().set(new Vector3(0, 1, 0));
                    break;
                case 3:
                    state.getNormal().set(new Vector3(0, -1, 0));
                    break;
                case 4:
                    state.getNormal().set(new Vector3(0, 0, 1));
                    break;
                case 5:
                    state.getNormal().set(new Vector3(0, 0, -1));
                    break;
                default:
                    state.getNormal().set(new Vector3(0, 0, 0));
                    break;
            }
            state.getGeoNormal().set(state.getNormal());
            state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal()));
            state.setShader(this);
        }

        public void intersectPrimitive(Ray r, int primID, IntersectionState state)
        {
            float intervalMin = float.NegativeInfinity;
            float intervalMax = float.PositiveInfinity;
            float orgX = r.ox;
            float invDirX = 1 / r.dx;
            float t1, t2;
            t1 = (minX - orgX) * invDirX;
            t2 = (maxX - orgX) * invDirX;
            int sideIn = -1, sideOut = -1;
            if (invDirX > 0)
            {
                if (t1 > intervalMin)
                {
                    intervalMin = t1;
                    sideIn = 0;
                }
                if (t2 < intervalMax)
                {
                    intervalMax = t2;
                    sideOut = 1;
                }
            }
            else
            {
                if (t2 > intervalMin)
                {
                    intervalMin = t2;
                    sideIn = 1;
                }
                if (t1 < intervalMax)
                {
                    intervalMax = t1;
                    sideOut = 0;
                }
            }
            if (intervalMin > intervalMax)
                return;
            float orgY = r.oy;
            float invDirY = 1 / r.dy;
            t1 = (minY - orgY) * invDirY;
            t2 = (maxY - orgY) * invDirY;
            if (invDirY > 0)
            {
                if (t1 > intervalMin)
                {
                    intervalMin = t1;
                    sideIn = 2;
                }
                if (t2 < intervalMax)
                {
                    intervalMax = t2;
                    sideOut = 3;
                }
            }
            else
            {
                if (t2 > intervalMin)
                {
                    intervalMin = t2;
                    sideIn = 3;
                }
                if (t1 < intervalMax)
                {
                    intervalMax = t1;
                    sideOut = 2;
                }
            }
            if (intervalMin > intervalMax)
                return;
            float orgZ = r.oz;
            float invDirZ = 1 / r.dz;
            t1 = (minZ - orgZ) * invDirZ; // no front wall
            t2 = (maxZ - orgZ) * invDirZ;
            if (invDirZ > 0)
            {
                if (t1 > intervalMin)
                {
                    intervalMin = t1;
                    sideIn = 4;
                }
                if (t2 < intervalMax)
                {
                    intervalMax = t2;
                    sideOut = 5;
                }
            }
            else
            {
                if (t2 > intervalMin)
                {
                    intervalMin = t2;
                    sideIn = 5;
                }
                if (t1 < intervalMax)
                {
                    intervalMax = t1;
                    sideOut = 4;
                }
            }
            if (intervalMin > intervalMax)
                return;
            Debug.Assert(sideIn != -1);
            Debug.Assert(sideOut != -1);
            // can't hit minY wall, there is none
            if (sideIn != 2 && r.isInside(intervalMin))
            {
                r.setMax(intervalMin);
                state.setIntersection(sideIn, 0, 0);
            }
            else if (sideOut != 2 && r.isInside(intervalMax))
            {
                r.setMax(intervalMax);
                state.setIntersection(sideOut, 0, 0);
            }
        }

        public Color getRadiance(ShadingState state)
        {
            int side = state.getPrimitiveID();
            Color kd = null;
            switch (side)
            {
                case 0:
                    kd = left;
                    break;
                case 1:
                    kd = right;
                    break;
                case 3:
                    kd = back;
                    break;
                case 4:
                    kd = bottom;
                    break;
                case 5:
                    float lx = state.getPoint().x;
                    float ly = state.getPoint().y;
                    if (lx >= lxmin && lx < lxmax && ly >= lymin && ly < lymax && state.getRay().dz > 0)
                        return state.includeLights ? radiance : Color.BLACK;
                    kd = top;
                    break;
                default:
                    Debug.Assert(false);
                    break;
            }
            // make sure we are on the right side of the material
            state.faceforward();
            // setup lighting
            state.initLightSamples();
            state.initCausticSamples();
            return state.diffuse(kd);
        }

        public void scatterPhoton(ShadingState state, Color power)
        {
            int side = state.getPrimitiveID();
            Color kd = null;
            switch (side)
            {
                case 0:
                    kd = left;
                    break;
                case 1:
                    kd = right;
                    break;
                case 3:
                    kd = back;
                    break;
                case 4:
                    kd = bottom;
                    break;
                case 5:
                    float lx = state.getPoint().x;
                    float ly = state.getPoint().y;
                    if (lx >= lxmin && lx < lxmax && ly >= lymin && ly < lymax && state.getRay().dz > 0)
                        return;
                    kd = top;
                    break;
                default:
                    Debug.Assert(false);
                    break;
            }
            // make sure we are on the right side of the material
            if (Vector3.dot(state.getNormal(), state.getRay().getDirection()) > 0)
            {
                state.getNormal().negate();
                state.getGeoNormal().negate();
            }
            state.storePhoton(state.getRay().getDirection(), power, kd);
            double avg = kd.getAverage();
            double rnd = state.getRandom(0, 0, 1);
            if (rnd < avg)
            {
                // photon is scattered
                power.mul(kd).mul(1 / (float)avg);
                OrthoNormalBasis onb = OrthoNormalBasis.makeFromW(state.getNormal());
                double u = 2 * Math.PI * rnd / avg;
                double v = state.getRandom(0, 1, 1);
                float s = (float)Math.Sqrt(v);
                float s1 = (float)Math.Sqrt(1.0 - v);
                Vector3 w = new Vector3((float)Math.Cos(u) * s, (float)Math.Sin(u) * s, s1);
                w = onb.transform(w, new Vector3());
                state.traceDiffusePhoton(new Ray(state.getPoint(), w), power);
            }
        }

        public int getNumSamples()
        {
            return samples;
        }

        public void getSamples(ShadingState state)
        {
            if (lightBounds.contains(state.getPoint()) && state.getPoint().z < maxZ)
            {
                int n = state.getDiffuseDepth() > 0 ? 1 : samples;
                float a = area / n;
                for (int i = 0; i < n; i++)
                {
                    // random offset on unit square, we use the infinite version of
                    // getRandom
                    // because the light sampling is adaptive
                    double randX = state.getRandom(i, 0);
                    double randY = state.getRandom(i, 1);

                    Point3 p = new Point3();
                    p.x = (float)(lxmin * (1 - randX) + lxmax * randX);
                    p.y = (float)(lymin * (1 - randY) + lymax * randY);
                    p.z = maxZ - 0.001f;

                    LightSample dest = new LightSample();
                    // prepare shadow ray to sampled point
                    dest.setShadowRay(new Ray(state.getPoint(), p));

                    // check that the direction of the sample is the same as the
                    // normal
                    float cosNx = dest.dot(state.getNormal());
                    if (cosNx <= 0)
                        return;

                    // light source facing point ?
                    // (need to check with light source's normal)
                    float cosNy = dest.getShadowRay().dz;
                    if (cosNy > 0)
                    {
                        // compute geometric attenuation and probability scale
                        // factor
                        float r = dest.getShadowRay().getMax();
                        float g = cosNy / (r * r);
                        float scale = g * a;
                        // set sample radiance
                        dest.setRadiance(radiance, radiance);
                        dest.getDiffuseRadiance().mul(scale);
                        dest.getSpecularRadiance().mul(scale);
                        dest.traceShadow(state);
                        state.addSample(dest);
                    }
                }
            }
        }

        public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power)
        {
            p.x = (float)(lxmin * (1 - randX2) + lxmax * randX2);
            p.y = (float)(lymin * (1 - randY2) + lymax * randY2);
            p.z = maxZ - 0.001f;

            double u = 2 * Math.PI * randX1;
            double s = Math.Sqrt(randY1);
            dir.set((float)(Math.Cos(u) * s), (float)(Math.Sin(u) * s), (float)-Math.Sqrt(1.0f - randY1));
            Color.mul((float)Math.PI * area, radiance, power);
        }

        public float getPower()
        {
            return radiance.copy().mul((float)Math.PI * area).getLuminance();
        }

        public int getNumPrimitives()
        {
            return 1;
        }

        public float getPrimitiveBound(int primID, int i)
        {
            switch (i)
            {
                case 0:
                    return minX;
                case 1:
                    return maxX;
                case 2:
                    return minY;
                case 3:
                    return maxY;
                case 4:
                    return minZ;
                case 5:
                    return maxZ;
                default:
                    return 0;
            }
        }

        public BoundingBox getWorldBounds(Matrix4 o2w)
        {
            BoundingBox bounds = new BoundingBox(minX, minY, minZ);
            bounds.include(maxX, maxY, maxZ);
            if (o2w == null)
                return bounds;
            return o2w.transform(bounds);
        }

        public PrimitiveList getBakingPrimitives()
        {
            return null;
        }
    }
}